/**
 * @name Alert suppression
 * @description Generates information about alert suppressions.
 * @kind alert-suppression
 * @id py/alert-suppression
 */

import python

/**
 * An alert suppression comment.
 */
abstract class SuppressionComment extends Comment {
    /** Gets the scope of this suppression. */
    abstract SuppressionScope getScope();

    /** Gets the suppression annotation in this comment. */
    abstract string getAnnotation();

    /**
     * Holds if this comment applies to the range from column `startcolumn` of line `startline`
     * to column `endcolumn` of line `endline` in file `filepath`.
     */
    abstract predicate covers(
        string filepath, int startline, int startcolumn, int endline, int endcolumn
    );
}

/**
 * An alert comment that applies to a single line
 */
abstract class LineSuppressionComment extends SuppressionComment {
    LineSuppressionComment() {
        exists(string filepath, int l |
            this.getLocation().hasLocationInfo(filepath, l, _, _, _) and
            any(AstNode a).getLocation().hasLocationInfo(filepath, l, _, _, _)
        )
    }

    /** Gets the scope of this suppression. */
    override SuppressionScope getScope() { result = this }

    override predicate covers(
        string filepath, int startline, int startcolumn, int endline, int endcolumn
    ) {
        this.getLocation().hasLocationInfo(filepath, startline, _, endline, endcolumn) and
        startcolumn = 1
    }
}

/**
 * An lgtm suppression comment.
 */
class LgtmSuppressionComment extends LineSuppressionComment {
    string annotation;

    LgtmSuppressionComment() {
        exists(string all | all = this.getContents() |
            // match `lgtm[...]` anywhere in the comment
            annotation = all.regexpFind("(?i)\\blgtm\\s*\\[[^\\]]*\\]", _, _)
            or
            // match `lgtm` at the start of the comment and after semicolon
            annotation = all.regexpFind("(?i)(?<=^|;)\\s*lgtm(?!\\B|\\s*\\[)", _, _).trim()
        )
    }

    /** Gets the suppression annotation in this comment. */
    override string getAnnotation() { result = annotation }
}

/**
 * A noqa suppression comment. Both pylint and pyflakes respect this, so lgtm ought to too.
 */
class NoqaSuppressionComment extends LineSuppressionComment {
    NoqaSuppressionComment() { this.getContents().toLowerCase().regexpMatch("\\s*noqa\\s*") }

    override string getAnnotation() { result = "lgtm" }
}

/**
 * The scope of an alert suppression comment.
 */
class SuppressionScope extends @py_comment {
    SuppressionScope() { this instanceof SuppressionComment }

    /**
     * Holds if this element is at the specified location.
     * The location spans column `startcolumn` of line `startline` to
     * column `endcolumn` of line `endline` in file `filepath`.
     * For more information, see
     * [Locations](https://help.semmle.com/QL/learn-ql/ql/locations.html).
     */
    predicate hasLocationInfo(
        string filepath, int startline, int startcolumn, int endline, int endcolumn
    ) {
        this.(SuppressionComment).covers(filepath, startline, startcolumn, endline, endcolumn)
    }

    /** Gets a textual representation of this element. */
    string toString() { result = "suppression range" }
}

from SuppressionComment c
select c, // suppression comment
    c.getContents(), // text of suppression comment (excluding delimiters)
    c.getAnnotation(), // text of suppression annotation
    c.getScope() // scope of suppression
